package com.weixin.api;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.thoughtworks.xstream.XStream;
import com.weixin.bean.*;
import com.weixin.bean.result.*;
import com.weixin.common.bean.WxAccessToken;
import com.weixin.common.bean.WxJsapiSignature;
import com.weixin.common.bean.WxMenu;
import com.weixin.common.bean.result.WxError;
import com.weixin.common.bean.result.WxMediaUploadResult;
import com.weixin.common.exception.WxErrorException;
import com.weixin.common.session.StandardSessionManager;
import com.weixin.common.session.WxSessionManager;
import com.weixin.common.util.PropertiesUtil;
import com.weixin.common.util.RandomUtils;
import com.weixin.common.util.StringUtils;
import com.weixin.common.util.crypto.SHA1;
import com.weixin.common.util.crypto.WxCryptUtil;
import com.weixin.common.util.fs.FileUtils;
import com.weixin.common.util.http.*;
import com.weixin.common.util.json.GsonHelper;
import com.weixin.common.util.xml.XStreamInitializer;
import com.weixin.util.WxConfig;
import com.weixin.util.http.QrCodeRequestExecutor;
import com.weixin.util.json.WxMpGsonBuilder;

import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.math.BigDecimal;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.util.*;

import javax.net.ssl.SSLContext;

import org.apache.http.HttpEntity;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.util.EntityUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class WxMpServiceImpl implements WxMpService {

	public WxMpServiceImpl()
	{
		super();
		WxMpInMemoryConfigStorage config = new WxMpInMemoryConfigStorage();
		config.setAppId(WxConfig.AppId);
		//		config.setAesKey(WxConfig.AppSecret);
		config.setSecret(WxConfig.AppSecret);
		config.setPartnerId(WxConfig.MchId);
		config.setPartnerKey(WxConfig.SignKey);
		this.setWxMpConfigStorage(config);
	}

	protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class);

	/**
	 * 全局的是否正在刷新access token的锁
	 */
	protected final Object globalAccessTokenRefreshLock = new Object();

	/**
	 * 全局的是否正在刷新jsapi_ticket的锁
	 */
	protected final Object globalJsapiTicketRefreshLock = new Object();

	protected WxMpConfigStorage wxMpConfigStorage;

	protected CloseableHttpClient httpClient;

	protected HttpHost httpProxy;

	private int retrySleepMillis = 1000;

	private int maxRetryTimes = 5;

	protected WxSessionManager sessionManager = new StandardSessionManager();

	public boolean checkSignature(String timestamp, String nonce, String signature) {
		try {
			return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature);
		} catch (Exception e) {
			return false;
		}
	}

	public String getAccessToken() throws WxErrorException {
		return getAccessToken(false);
	}

	public String getAccessToken(boolean forceRefresh) throws WxErrorException {
		if (forceRefresh) {
			wxMpConfigStorage.expireAccessToken();
		}
		if (wxMpConfigStorage.isAccessTokenExpired()) {
			synchronized (globalAccessTokenRefreshLock) {
				if (wxMpConfigStorage.isAccessTokenExpired()) {
					String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
							+ "&appid=" + wxMpConfigStorage.getAppId()
							+ "&secret=" + wxMpConfigStorage.getSecret()
							;
					try {
						HttpGet httpGet = new HttpGet(url);
						if (httpProxy != null) {
							RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
							httpGet.setConfig(config);
						}
						CloseableHttpClient httpclient = getHttpclient();
						CloseableHttpResponse response = httpclient.execute(httpGet);
						String resultContent = new BasicResponseHandler().handleResponse(response);
						WxError error = WxError.fromJson(resultContent);
						if (error.getErrorCode() != 0) {
							throw new WxErrorException(error);
						}
						WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
						wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
					} catch (ClientProtocolException e) {
						throw new RuntimeException(e);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			}
		}
		return wxMpConfigStorage.getAccessToken();
	}

	public String getJsapiTicket() throws WxErrorException {
		return getJsapiTicket(false);
	}

	public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
		if (forceRefresh) {
			wxMpConfigStorage.expireJsapiTicket();
		}
		if (wxMpConfigStorage.isJsapiTicketExpired()) {
			synchronized (globalJsapiTicketRefreshLock) {
				if (wxMpConfigStorage.isJsapiTicketExpired()) {
					String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi";
					String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
					JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
					JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject();
					String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
					int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
					wxMpConfigStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds);
				}
			}
		}
		return wxMpConfigStorage.getJsapiTicket();
	}

	public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
		long timestamp = System.currentTimeMillis() / 1000;
		String noncestr = RandomUtils.getRandomStr();
		String jsapiTicket = getJsapiTicket(false);
		try {
			String signature = SHA1.genWithAmple(
					"jsapi_ticket=" + jsapiTicket,
					"noncestr=" + noncestr,
					"timestamp=" + timestamp,
					"url=" + url
					);
			WxJsapiSignature jsapiSignature = new WxJsapiSignature();
			jsapiSignature.setTimestamp(timestamp);
			jsapiSignature.setNoncestr(noncestr);
			jsapiSignature.setUrl(url);
			jsapiSignature.setSignature(signature);
			return jsapiSignature;
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
	}

	public void customMessageSend(WxMpCustomMessage message) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
		execute(new SimplePostRequestExecutor(), url, message.toJson());
	}

	public void menuCreate(WxMenu menu) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/menu/create";
		execute(new SimplePostRequestExecutor(), url, menu.toJson());
	}

	public void menuDelete() throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/menu/delete";
		execute(new SimpleGetRequestExecutor(), url, null);
	}

	public WxMenu menuGet() throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/menu/get";
		try {
			String resultContent = execute(new SimpleGetRequestExecutor(), url, null);
			return WxMenu.fromJson(resultContent);
		} catch (WxErrorException e) {
			// 46003 不存在的菜单数据
			if (e.getError().getErrorCode() == 46003) {
				return null;
			}
			throw e;
		}
	}

	public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException {
		return mediaUpload(mediaType,FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
	}

	public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
		String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
		return execute(new MediaUploadRequestExecutor(), url, file);
	}

	public File mediaDownload(String media_id) throws WxErrorException {
		String url = "http://file.api.weixin.qq.com/cgi-bin/media/get";
		return execute(new MediaDownloadRequestExecutor(), url, "media_id=" + media_id);
	}

	public WxMpMassUploadResult massNewsUpload(WxMpMassNews news) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews";
		String responseContent = execute(new SimplePostRequestExecutor(), url, news.toJson());
		return WxMpMassUploadResult.fromJson(responseContent);
	}

	public WxMpMassUploadResult massVideoUpload(WxMpMassVideo video) throws WxErrorException {
		String url = "http://file.api.weixin.qq.com/cgi-bin/media/uploadvideo";
		String responseContent = execute(new SimplePostRequestExecutor(), url, video.toJson());
		return WxMpMassUploadResult.fromJson(responseContent);
	}

	public WxMpMassSendResult massGroupMessageSend(WxMpMassGroupMessage message) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall";
		String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
		return WxMpMassSendResult.fromJson(responseContent);
	}

	public WxMpMassSendResult massOpenIdsMessageSend(WxMpMassOpenIdsMessage message) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/message/mass/send";
		String responseContent = execute(new SimplePostRequestExecutor(), url, message.toJson());
		return WxMpMassSendResult.fromJson(responseContent);
	}

	public WxMpGroup groupCreate(String name) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/groups/create";
		JsonObject json = new JsonObject();
		JsonObject groupJson = new JsonObject();
		json.add("group", groupJson);
		groupJson.addProperty("name", name);

		String responseContent = execute(
				new SimplePostRequestExecutor(), 
				url, 
				json.toString());
		return WxMpGroup.fromJson(responseContent);
	}

	public List<WxMpGroup> groupGet() throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/groups/get";
		String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
		/*
		 * 操蛋的微信API，创建时返回的是 { group : { id : ..., name : ...} }
		 * 查询时返回的是 { groups : [ { id : ..., name : ..., count : ... }, ... ] }
		 */
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("groups"), new TypeToken<List<WxMpGroup>>(){}.getType());
	}

	public long userGetGroup(String openid) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/groups/getid";
		JsonObject o = new JsonObject();
		o.addProperty("openid", openid);
		String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return GsonHelper.getAsLong(tmpJsonElement.getAsJsonObject().get("groupid"));
	}

	public void groupUpdate(WxMpGroup group) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/groups/update";
		execute(new SimplePostRequestExecutor(), url, group.toJson());
	}

	public void userUpdateGroup(String openid, long to_groupid) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/groups/members/update";
		JsonObject json = new JsonObject();
		json.addProperty("openid", openid);
		json.addProperty("to_groupid", to_groupid);
		execute(new SimplePostRequestExecutor(), url, json.toString());
	}

	public void userUpdateRemark(String openid, String remark) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark";
		JsonObject json = new JsonObject();
		json.addProperty("openid", openid);
		json.addProperty("remark", remark);
		execute(new SimplePostRequestExecutor(), url, json.toString());
	}

	public WxMpUser userInfo(String openid, String lang) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/user/info";
		lang = lang == null ? "zh_CN" : lang;
		String responseContent = execute(new SimpleGetRequestExecutor(), url, "openid=" + openid + "&lang=" + lang);
		return WxMpUser.fromJson(responseContent);
	}

	public WxMpUserList userList(String next_openid) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/user/get";
		String responseContent = execute(new SimpleGetRequestExecutor(), url, next_openid == null ? null : "next_openid=" + next_openid);
		return WxMpUserList.fromJson(responseContent);
	}

	public WxMpQrCodeTicket qrCodeCreateTmpTicket(int scene_id, Integer expire_seconds) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
		JsonObject json = new JsonObject();
		json.addProperty("action_name", "QR_SCENE");
		if(expire_seconds != null) {
			json.addProperty("expire_seconds", expire_seconds);
		}
		JsonObject actionInfo = new JsonObject();
		JsonObject scene = new JsonObject();
		scene.addProperty("scene_id", scene_id);
		actionInfo.add("scene", scene);
		json.add("action_info", actionInfo);
		String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
		return WxMpQrCodeTicket.fromJson(responseContent);
	}

	public WxMpQrCodeTicket qrCodeCreateLastTicket(int scene_id) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
		JsonObject json = new JsonObject();
		json.addProperty("action_name", "QR_LIMIT_SCENE");
		JsonObject actionInfo = new JsonObject();
		JsonObject scene = new JsonObject();
		scene.addProperty("scene_id", scene_id);
		actionInfo.add("scene", scene);
		json.add("action_info", actionInfo);
		String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
		return WxMpQrCodeTicket.fromJson(responseContent);
	}

	public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create";
		JsonObject json = new JsonObject();
		json.addProperty("action_name", "QR_LIMIT_STR_SCENE");
		JsonObject actionInfo = new JsonObject();
		JsonObject scene = new JsonObject();
		scene.addProperty("scene_str", scene_str);
		actionInfo.add("scene", scene);
		json.add("action_info", actionInfo);
		String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString());
		return WxMpQrCodeTicket.fromJson(responseContent);	
	}

	public File qrCodePicture(WxMpQrCodeTicket ticket) throws WxErrorException {
		String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode";
		return execute(new QrCodeRequestExecutor(), url, ticket);
	}

	public String shortUrl(String long_url) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/shorturl";
		JsonObject o = new JsonObject();
		o.addProperty("action", "long2short");
		o.addProperty("long_url", long_url);
		String responseContent = execute(new SimplePostRequestExecutor(), url, o.toString());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return tmpJsonElement.getAsJsonObject().get("short_url").getAsString();
	}

	public String templateSend(WxMpTemplateMessage templateMessage) throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/message/template/send";
		String responseContent = execute(new SimplePostRequestExecutor(), url, templateMessage.toJson());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return tmpJsonElement.getAsJsonObject().get("msgid").getAsString();
	}

	public WxMpSemanticQueryResult semanticQuery(WxMpSemanticQuery semanticQuery) throws WxErrorException {
		String url = "https://api.weixin.qq.com/semantic/semproxy/search";
		String responseContent = execute(new SimplePostRequestExecutor(), url, semanticQuery.toJson());
		return WxMpSemanticQueryResult.fromJson(responseContent);
	}

	@Override
	public String oauth2buildAuthorizationUrl(String scope, String state) {
		return this.oauth2buildAuthorizationUrl(wxMpConfigStorage.getOauth2redirectUri(), scope, state);
	}

	@Override
	public String oauth2buildAuthorizationUrl(String redirectURI, String scope, String state) {
		String url = "https://open.weixin.qq.com/connect/oauth2/authorize?" ;
		url += "appid=" + wxMpConfigStorage.getAppId();
		url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectURI);
		url += "&response_type=code";
		url += "&scope=" + scope;
		if (state != null) {
			url += "&state=" + state;
		}
		url += "#wechat_redirect";
		return url;
	}

	@Override
	public WxMpOAuth2AccessToken oauth2getAccessToken(String code) throws WxErrorException {
		String url = "https://api.weixin.qq.com/sns/oauth2/access_token?";
		url += "appid=" +  wxMpConfigStorage.getAppId();
		url += "&secret=" + wxMpConfigStorage.getSecret();
		url += "&code=" + code;
		url += "&grant_type=authorization_code";

		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
			return WxMpOAuth2AccessToken.fromJson(responseText);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public WxMpOAuth2AccessToken oauth2refreshAccessToken(String refreshToken) throws WxErrorException {
		String url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?";
		url += "appid=" + wxMpConfigStorage.getAppId();
		url += "&grant_type=refresh_token";
		url += "&refresh_token=" + refreshToken;

		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
			return WxMpOAuth2AccessToken.fromJson(responseText);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public WxMpUser oauth2getUserInfo(WxMpOAuth2AccessToken oAuth2AccessToken, String lang) throws WxErrorException {
		String url = "https://api.weixin.qq.com/sns/userinfo?";
		url += "access_token=" + oAuth2AccessToken.getAccessToken();
		url += "&openid=" + oAuth2AccessToken.getOpenId();
		if (lang == null) {
			url += "&lang=zh_CN";
		} else {
			url += "&lang=" + lang;
		}

		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			String responseText = executor.execute(getHttpclient(), httpProxy, url, null);
			return WxMpUser.fromJson(responseText);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public boolean oauth2validateAccessToken(WxMpOAuth2AccessToken oAuth2AccessToken) {
		String url = "https://api.weixin.qq.com/sns/auth?";
		url += "access_token=" + oAuth2AccessToken.getAccessToken();
		url += "&openid=" + oAuth2AccessToken.getOpenId();

		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			executor.execute(getHttpclient(), httpProxy, url, null);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} catch (WxErrorException e) {
			return false;
		}
		return true;
	}

	@Override
	public String[] getCallbackIP() throws WxErrorException {
		String url = "https://api.weixin.qq.com/cgi-bin/getcallbackip";
		String responseContent = get(url, null);
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		JsonArray ipList = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray();
		String[] ipArray = new String[ipList.size()];
		for (int i = 0; i < ipList.size(); i++) {
			ipArray[i] = ipList.get(i).getAsString();
		}
		return ipArray;
	}


	@Override
	public List<WxMpUserSummary> getUserSummary(Date beginDate, Date endDate) throws WxErrorException {
		String url = "https://api.weixin.qq.com/datacube/getusersummary";
		JsonObject param = new JsonObject();
		param.addProperty("begin_date", SIMPLE_DATE_FORMAT.format(beginDate));
		param.addProperty("end_date", SIMPLE_DATE_FORMAT.format(endDate));
		String responseContent = post(url, param.toString());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("list"), new TypeToken<List<WxMpUserSummary>>(){}.getType());
	}

	@Override
	public List<WxMpUserCumulate> getUserCumulate(Date beginDate, Date endDate) throws WxErrorException {
		String url = "https://api.weixin.qq.com/datacube/getusercumulate";
		JsonObject param = new JsonObject();
		param.addProperty("begin_date", SIMPLE_DATE_FORMAT.format(beginDate));
		param.addProperty("end_date", SIMPLE_DATE_FORMAT.format(endDate));
		String responseContent = post(url, param.toString());
		JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent)));
		return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement.getAsJsonObject().get("list"), new TypeToken<List<WxMpUserCumulate>>(){}.getType());
	}

	public String get(String url, String queryParam) throws WxErrorException {
		return execute(new SimpleGetRequestExecutor(), url, queryParam);
	}

	public String post(String url, String postData) throws WxErrorException {
		return execute(new SimplePostRequestExecutor(), url, postData);
	}

	/**
	 * 向微信端发送请求，在这里执行的策略是当发生access_token过期时才去刷新，然后重新执行请求，而不是全局定时请求
	 * @param executor
	 * @param uri
	 * @param data
	 * @return
	 * @throws WxErrorException
	 */
	public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
		int retryTimes = 0;
		do {
			try {
				return executeInternal(executor, uri, data);
			} catch (WxErrorException e) {
				WxError error = e.getError();
				/**
				 * -1 系统繁忙, 1000ms后重试
				 */
				if (error.getErrorCode() == -1) {
					int sleepMillis = retrySleepMillis * (1 << retryTimes);
					try {
						log.debug("微信系统繁忙，{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
						Thread.sleep(sleepMillis);
					} catch (InterruptedException e1) {
						throw new RuntimeException(e1);
					}
				} else {
					throw e;
				}
			}
		} while(++retryTimes < maxRetryTimes);

		throw new RuntimeException("微信服务端异常，超出重试次数");
	}

	protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
		String accessToken = getAccessToken(false);

		String uriWithAccessToken = uri;
		uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;

		try {
			return executor.execute(getHttpclient(), httpProxy, uriWithAccessToken, data);
		} catch (WxErrorException e) {
			WxError error = e.getError();
			/*
			 * 发生以下情况时尝试刷新access_token
			 * 40001 获取access_token时AppSecret错误，或者access_token无效
			 * 42001 access_token超时
			 */
			if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) {
				// 强制设置wxMpConfigStorage它的access token过期了，这样在下一次请求里就会刷新access token
				wxMpConfigStorage.expireAccessToken();
				return execute(executor, uri, data);
			}
			if (error.getErrorCode() != 0) {
				throw new WxErrorException(error);
			}
			return null;
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	protected CloseableHttpClient getHttpclient() {
		return httpClient;
	}

	public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider) {
		this.wxMpConfigStorage = wxConfigProvider;

		String http_proxy_host = wxMpConfigStorage.getHttp_proxy_host();
		int http_proxy_port = wxMpConfigStorage.getHttp_proxy_port();
		String http_proxy_username = wxMpConfigStorage.getHttp_proxy_username();
		String http_proxy_password = wxMpConfigStorage.getHttp_proxy_password();

		if(StringUtils.isNotBlank(http_proxy_host)) {
			// 使用代理服务器
			if(StringUtils.isNotBlank(http_proxy_username)) {
				// 需要用户认证的代理服务器
				CredentialsProvider credsProvider = new BasicCredentialsProvider();
				credsProvider.setCredentials(
						new AuthScope(http_proxy_host, http_proxy_port),
						new UsernamePasswordCredentials(http_proxy_username, http_proxy_password));
				httpClient = HttpClients
						.custom()
						.setDefaultCredentialsProvider(credsProvider)
						.build();
			} else {
				// 无需用户认证的代理服务器
				httpClient = HttpClients.createDefault();
			}
			httpProxy = new HttpHost(http_proxy_host, http_proxy_port);
		} else {
			httpClient = HttpClients.createDefault();
		}
	}

	@Override
	public void setRetrySleepMillis(int retrySleepMillis) {
		this.retrySleepMillis = retrySleepMillis;
	}


	@Override
	public void setMaxRetryTimes(int maxRetryTimes) {
		this.maxRetryTimes = maxRetryTimes;
	}

	@Override
	public WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, float amt, String body, String tradeType, String ip, String callbackUrl) {
		String nonce_str = System.currentTimeMillis() + "";

		SortedMap<String, String> packageParams = new TreeMap<String, String>();
		packageParams.put("appid", wxMpConfigStorage.getAppId());
		packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
		packageParams.put("nonce_str", nonce_str);
		packageParams.put("body", body);
		packageParams.put("out_trade_no", outTradeNo);

		int   total_fee  =   (new   Float(amt*100.0)).intValue();
		packageParams.put("total_fee",total_fee+"");
		System.out.println("total_fee:"+total_fee);
		packageParams.put("spbill_create_ip", ip);
		packageParams.put("notify_url", callbackUrl);
		packageParams.put("trade_type", tradeType);
//		packageParams.put("openid", openId);

		String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey());
		String xml = "<xml>" +
				"<appid>" + wxMpConfigStorage.getAppId() + "</appid>" +
				"<mch_id>" + wxMpConfigStorage.getPartnerId() + "</mch_id>" +
				"<nonce_str>" + nonce_str + "</nonce_str>" +
				"<sign>" + sign + "</sign>" +
				"<body><![CDATA[" + body + "]]></body>" +
				"<out_trade_no>" + outTradeNo + "</out_trade_no>" +
				"<total_fee>" + packageParams.get("total_fee") + "</total_fee>" +
				"<spbill_create_ip>" + ip + "</spbill_create_ip>" +
				"<notify_url>" + callbackUrl + "</notify_url>" +
				"<trade_type>" + tradeType + "</trade_type>" +
//				"<openid>" + openId + "</openid>" +
				"</xml>";
		//System.out.print("xml:"+xml);
		HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder");
		if (httpProxy != null) {
			RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
			httpPost.setConfig(config);
		}

		StringEntity entity = new StringEntity(xml, Consts.UTF_8);
		httpPost.setEntity(entity);
		try {
			CloseableHttpResponse response = httpClient.execute(httpPost);
			String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
			XStream xstream = XStreamInitializer.getInstance();
			xstream.alias("xml", WxMpPrepayIdResult.class);
			WxMpPrepayIdResult wxMpPrepayIdResult = (WxMpPrepayIdResult) xstream.fromXML(responseContent);
			return wxMpPrepayIdResult;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return new WxMpPrepayIdResult();
	}

	@Override
	public Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) {
		WxMpPrepayIdResult wxMpPrepayIdResult = getPrepayId(openId, outTradeNo, (float)amt, body, tradeType, ip, callbackUrl);
		String prepayId = wxMpPrepayIdResult.getPrepay_id();
		if (prepayId == null || prepayId.equals("")) {
			throw new RuntimeException("get prepayid error");
		}

		Map<String, String> payInfo = new HashMap<String, String>();
		payInfo.put("appId", wxMpConfigStorage.getAppId());
		// 支付签名时间戳，注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
		payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
		payInfo.put("nonceStr", System.currentTimeMillis() + "");
		payInfo.put("package", "prepay_id=" + prepayId);
		payInfo.put("signType", "MD5");

		String finalSign = WxCryptUtil.createSign(payInfo, wxMpConfigStorage.getPartnerKey());
		payInfo.put("sign", finalSign);
		return payInfo;
	}

	@Override
	public NotifyMpPayResult queryOrder(String out_trade_no,String transaction_id) {
		String nonce_str = System.currentTimeMillis() + "";

		SortedMap<String, String> packageParams = new TreeMap<String, String>();
		packageParams.put("appid", wxMpConfigStorage.getAppId());
		packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
		packageParams.put("nonce_str", nonce_str);
		packageParams.put("out_trade_no", out_trade_no);

		String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey());
		String xml = "<xml>" +
				"<appid>" + wxMpConfigStorage.getAppId() + "</appid>" +
				"<mch_id>" + wxMpConfigStorage.getPartnerId() + "</mch_id>" +
				"<nonce_str>" + nonce_str + "</nonce_str>" +
				"<out_trade_no>" + out_trade_no + "</out_trade_no>"+
				"<transaction_id>"+transaction_id+"</transaction_id>"+
				"<sign>" + sign + "</sign>" +
				"</xml>";
		//System.out.print("xml:"+xml);



		HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/orderquery");
		if (httpProxy != null) {
			RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
			httpPost.setConfig(config);
		}

		StringEntity entity = new StringEntity(xml, Consts.UTF_8);
		httpPost.setEntity(entity);
		try {
			CloseableHttpResponse response = httpClient.execute(httpPost);
			String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
			XStream xstream = XStreamInitializer.getInstance();
			xstream.alias("xml", NotifyMpPayResult.class);
			NotifyMpPayResult result = (NotifyMpPayResult) xstream.fromXML(responseContent);
			return result;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	@SuppressWarnings("deprecation")
	@Override
	public RefundResData refund(String outRefundNo,String transactionID, String outTradeNo,
			String deviceInfo, int totalFee, int refundFee,String refundFeeType) {

		try {
			String nonce_str = System.currentTimeMillis() + "";

			SortedMap<String, String> packageParams = new TreeMap<String, String>();
			packageParams.put("appid", wxMpConfigStorage.getAppId());
			packageParams.put("mch_id", wxMpConfigStorage.getPartnerId());
			packageParams.put("nonce_str", nonce_str);
			packageParams.put("op_user_id", wxMpConfigStorage.getPartnerId());
			packageParams.put("out_refund_no", outRefundNo);
			packageParams.put("out_trade_no", outTradeNo);
			packageParams.put("refund_fee", refundFee+"");
			packageParams.put("total_fee", totalFee+"");
			packageParams.put("transaction_id", transactionID);

			String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey());
			String xml = "<xml>" +
					"<appid>" + wxMpConfigStorage.getAppId() + "</appid>" +
					"<mch_id>" + wxMpConfigStorage.getPartnerId() + "</mch_id>" +
					"<nonce_str>" + nonce_str + "</nonce_str>" +
					"<op_user_id>"+wxMpConfigStorage.getPartnerId() +"</op_user_id>"+
					"<out_refund_no>"+outRefundNo+"</out_refund_no>"+
					"<out_trade_no>"+outTradeNo+"</out_trade_no>"+
					"<refund_fee>"+refundFee+"</refund_fee>"+
					"<total_fee>"+totalFee+"</total_fee>"+
					"<transaction_id>"+transactionID+"</transaction_id>"+
					"<sign>" + sign + "</sign>"+
					"</xml>";
			System.out.print("xml:"+xml);

			KeyStore keyStore  = KeyStore.getInstance("PKCS12");
			String cerPath="";
			try {
				cerPath = PropertiesUtil.getProperty(PropertiesUtil.read(this.getClass().getResource("/").getPath() +"/config.properties"), "cerPath");
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
//			FileInputStream instream = new FileInputStream(new File("D:/platform/cert/apiclient_cert.p12"));
			FileInputStream instream = new FileInputStream(new File(cerPath));
			try {
				keyStore.load(instream, WxConfig.MchId.toCharArray());
			} finally {
				instream.close();
			}

			// Trust own CA and all self-signed certs
			SSLContext sslcontext = SSLContexts.custom()
					.loadKeyMaterial(keyStore, WxConfig.MchId.toCharArray())
					.build();
			// Allow TLSv1 protocol only
			SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
					sslcontext,
					new String[] { "TLSv1" },
					null,
					SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
			CloseableHttpClient sslClient = HttpClients.custom()
					.setSSLSocketFactory(sslsf)
					.build();

			HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/secapi/pay/refund");
			if (httpProxy != null) {
				RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
				httpPost.setConfig(config);
			}

			StringEntity entity = new StringEntity(xml, Consts.UTF_8);
			httpPost.setEntity(entity);
			try {
				CloseableHttpResponse response = sslClient.execute(httpPost);
				String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
				XStream xstream = XStreamInitializer.getInstance();
				xstream.alias("xml", RefundResData.class);
				RefundResData resData = (RefundResData) xstream.fromXML(responseContent);
				return resData;
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return new RefundResData();
	}
}
